package com.rgbgame.game;

import processing.core.PApplet;

import java.awt.Color;

/**
 * 
 */

public final class Particle {
	/**
	 * 
	 */

    /** static instance references **/
    PApplet p = rgbGame.pApplet;
    rgbGame g = rgbGame.game;


    public Color myColor;
	boolean pinned;
	private Vector3d pinPos;
	public Vector3d velocity; // no velocity in a velocity-free Verlet
	// scheme - need to specially init it!
	private float lastdelta; // stored for finding velocity
	public Vector3d force;
	public Vector3d position;
	public Vector3d positionLast;
	public Vector3d truePositionLast; // the actual position at the
	// previous time step - saved
	// separately from positionLast,
	// which may be altered to change
	// velocity
	public float mass;
	public boolean isDrawn;
	public float radius;
	public float alphaValue;
	public boolean isCollided;
	private int id;
	private boolean lineDraw;
	private boolean isPlayer;
	private float airDrag;
	public boolean stuck;
	private Vector3d stuckPosition;
	private float unstickDist;
	
	//Scripting variables
	private boolean scripted;
	private Vector3d[] scriptedPosition;
	private float[] scriptedTime;
	private float currentScriptTime;
	private int currentScriptIndex;
	
	public void setScript(Vector3d[] _p, float[] _t){
		scripted = true;
		scriptedPosition = _p;
		scriptedTime = _t;
		currentScriptTime = 0f;
		currentScriptIndex = 0;
	}

	public int getID() {
		return id;
	}

	public void createID() {
		id = g.globalParticleList.size();
		// println(id);
		g.globalParticleList.add(this);
	}

	{
		alphaValue = 255.0f;
		pinned = false;
		isCollided = true;
		id = -1;
		truePositionLast = new Vector3d(0, 0, 0);
		velocity = new Vector3d(0, 0, 0);
		lineDraw = false;
		isPlayer = false;
		airDrag = .3f;
		stuck = false;
		unstickDist = 0f;
		scripted = false;
	}
	
	public void stickParticle(float sDist){
		stuck = true;
		stuckPosition = new Vector3d(position);
		positionLast.set(stuckPosition);
		unstickDist = sDist;
	}

	public void setAirDrag(float newDrag) {
		airDrag = newDrag;
	}

	public void setPlayer(boolean isP) {
		isPlayer = isP;
	}

	public boolean isPlayer() {
		return isPlayer;
	}

	public void setPinned() {
		pinned = true;
		mass = 1f;
		pinPos = new Vector3d(position);
	}

	public void unsetPinned() {
		pinned = false;
	}

	public void constrainBox(Vector3d minVec, Vector3d maxVec) {
		position.x = PApplet.constrain(position.x, minVec.x, maxVec.x);
		position.y = PApplet.constrain(position.y, minVec.y, maxVec.y);
		position.z = PApplet.constrain(position.z, minVec.z, maxVec.z);
	}

	public Particle(Particle p) {
		this(p.position, p.positionLast, p.mass, p.radius, p.myColor);
		alphaValue = p.alphaValue;
		pinned = p.pinned;
		// createID();
	}

	public Particle(Vector3d _position, Vector3d _positionLast, float mass,
                    float radius, Color col) {
		position = new Vector3d(_position);
		positionLast = new Vector3d(_positionLast);
		truePositionLast = new Vector3d(_positionLast);
		force = new Vector3d(0, 0, 0);
		this.mass = mass;
		isDrawn = true;
		this.radius = radius;
		myColor = col;
		lastdelta = g.defaultDelta;
		// debugCounter++;
		// createID();
	}

	public Particle(Vector3d _position, float mass, float radius, Color col) {
		this(_position, _position, mass, radius, col);
	}

	public Particle(Vector3d _position, Vector3d _positionLast, float m) {
		position = new Vector3d(_position);
		positionLast = new Vector3d(_positionLast);
		truePositionLast = new Vector3d(_positionLast);
		force = new Vector3d(0, 0, 0);
		mass = m;
		isDrawn = true;
		radius = g.defaultRadius;
		myColor = g.defaultColor;
		lastdelta = g.defaultDelta;
		// debugCounter++;
		// createID();
	}

	public Particle(Vector3d _pos, float mass) {
		position = new Vector3d(_pos);
		positionLast = new Vector3d(_pos);
		truePositionLast = new Vector3d(_pos);
		force = new Vector3d(0, 0, 0);
		this.mass = mass;
		isDrawn = true;
		radius = g.defaultRadius;
		myColor = g.defaultColor;
		lastdelta = g.defaultDelta;
		// debugCounter++;
		// createID();
	}

	public void setLastDelta(float myDelta) {
		lastdelta = myDelta;
	}

	public void setRadius(float newRad) {
		radius = newRad;
	}

	public void setDraw(boolean tf) {
		isDrawn = tf;
	}

	public void draw(float myAlpha) {
		if (isDrawn) {
			float factor = 1.0f * PApplet.max(rgbGame.width, rgbGame.height);
			if ((-rgbGame.game.cameraX < (PApplet.min(position.x, truePositionLast.x) - factor))
					|| (-rgbGame.game.cameraX > (PApplet.max(position.x, truePositionLast.x) + factor))) {
				return;
			}
			if ((-rgbGame.game.cameraY < (PApplet.min(position.y, truePositionLast.y) - factor))
					|| (-rgbGame.game.cameraY > (PApplet.max(position.y, truePositionLast.y) + factor))) {
				return;
			}
			if (lineDraw) {
				// stroke(color(myColor.x,myColor.y,myColor.z,myAlpha));
				// line(position.x,position.y,truePositionLast.x,truePositionLast.y);
				//if (this.game.graphicsMode.equals(game.OPENGL)){
				//	this.game.gl.glBegin(GL.GL_POINTS);
				//	this.game.gl.glColor3f(75 / 255.0f, 75 / 255.0f, 75 / 255.0f);
					
				//}else{
					p.beginShape(PApplet.LINES);
					p.stroke(myColor.getRed(), myColor.getGreen(), myColor.getBlue(), myAlpha);
					p.vertex(position.x, position.y);
					p.stroke(myColor.getRed(), myColor.getGreen(), myColor.getBlue(), 0);
					p.vertex(truePositionLast.x, truePositionLast.y);
					p.endShape();
				//}
			} else {
				p.fill(myColor.getRed(), myColor.getGreen(), myColor.getBlue(), myAlpha);
				g.circle(position, radius);
			}
		}
	}

	public void draw() {
        if (isDrawn) {
			float factor = 1.0f * PApplet.max(rgbGame.width, rgbGame.height);
			if ((-rgbGame.game.cameraX < (PApplet.min(position.x, truePositionLast.x) - factor))
					|| (-rgbGame.game.cameraX > (PApplet.max(position.x, truePositionLast.x) + factor))) {
				return;
			}
			if ((-rgbGame.game.cameraY < (PApplet.min(position.y, truePositionLast.y) - factor))
					|| (-rgbGame.game.cameraY > (PApplet.max(position.y, truePositionLast.y) + factor))) {
				return;
			}

			if (lineDraw) {
				// stroke(color(myColor.x,myColor.y,myColor.z,alphaValue));
				// line(position.x,position.y,truePositionLast.x,truePositionLast.y);
				p.beginShape(PApplet.LINES);
				p.stroke(p.color(myColor.getRed(), myColor.getGreen(), myColor.getBlue(), alphaValue));
				p.vertex(position.x, position.y);
				p.stroke(p.color(myColor.getRed(), myColor.getGreen(), myColor.getBlue(), 1));
				p.vertex(truePositionLast.x, truePositionLast.y);
				p.endShape();
			} else {
				if (alphaValue < 254.9f) {
					p.fill(myColor.getRed(), myColor.getGreen(), myColor.getBlue(), alphaValue);
				} else {
					p.fill(myColor.getRed(), myColor.getGreen(), myColor.getBlue());
				}

				g.circle(position, radius);
			}
		}
	}

	public void forceAdd(Vector3d toAdd) {
		force.add(toAdd);
	}

	public void forceSubtract(Vector3d toSub) {
		force.subtract(toSub);
	}

	public void forceZero() {
		force.set(0.0f, 0.0f, 0.0f);
	}

	public void setVel() { // this is naive right now - change to include
		// force information
		// velocity = position.minus(positionLast).times(1.0/lastdelta);
		velocity.x = (position.x - positionLast.x) / lastdelta;
		velocity.y = (position.y - positionLast.y) / lastdelta;
	}
	
	public void scriptedTimeStep(float delta){
		positionLast.set(position);
		truePositionLast.set(position);
		int num = scriptedTime.length;
		if (num > 1){
			//System.out.println("t: "+currentScriptTime);
			float earlierTime = scriptedTime[currentScriptIndex];
			float earlierX = scriptedPosition[currentScriptIndex].x;
			float earlierY = scriptedPosition[currentScriptIndex].y;
			float laterTime,laterX,laterY;
			if (currentScriptIndex<=num-2){
				laterTime = scriptedTime[currentScriptIndex+1];
				laterX = scriptedPosition[currentScriptIndex+1].x;
				laterY = scriptedPosition[currentScriptIndex+1].y;
			} else{ //we need to loop around
				//Assume that time difference is the same as last step
				laterTime = earlierTime + earlierTime-scriptedTime[currentScriptIndex-1];
				laterX = scriptedPosition[0].x;
				laterY = scriptedPosition[0].y;
			}
			float propTime = (currentScriptTime-earlierTime)/(laterTime-earlierTime);
			//System.out.println(propTime);
			//System.out.println(currentScriptIndex+" "+scriptedPosition[currentScriptIndex]+"-"+scriptedPosition[currentScriptIndex+1]);
			position.x = propTime*laterX+(1.0f-propTime)*earlierX;
			position.y = propTime*laterY+(1.0f-propTime)*earlierY;
			currentScriptTime += delta;
			if (currentScriptTime >= scriptedTime[num-1]){
				currentScriptTime = currentScriptTime%scriptedTime[num-1];
				currentScriptIndex = 0;
			}
			float nextTime = scriptedTime[currentScriptIndex+1];
			//NOT WORKING - HANDLE THIS RIGHT
			while (currentScriptTime > nextTime){
				currentScriptIndex++;
				nextTime = scriptedTime[currentScriptIndex+1];
			}
		} else if (scriptedTime.length == 1){
			position.set(scriptedPosition[0]);
		}
	}

	public void timeStep(float delta) {
        if (scripted){
        	scriptedTimeStep(delta);
        	return;
        }
		if (pinned) {
			position.set(pinPos);
			positionLast.set(pinPos);
			truePositionLast.set(pinPos);
			return;
		} else {
			// debugCounter++;
			// float airDrag = .3;
			setVel();
			// force.add(velocity.times(-airDrag*mass));
			force.x += velocity.x * (-airDrag * mass);
			force.y += velocity.y * (-airDrag * mass);

			// com.rgbgame.game.Vector3d accel = force.times(1.0/mass);
			// com.rgbgame.game.Vector3d buffer = new com.rgbgame.game.Vector3d(position);
			//g.bufferA.set(force);
			//g.bufferA.multiply(1.0f / mass);
			float bufferAx = force.x / mass;
			float bufferAy = force.y / mass;
			//g.bufferB.set(position);
			float bufferBx = position.x;
			float bufferBy = position.y;

			// remove this check eventually...hasn't been tripped in a long time
	/*		if (Float.isNaN(bufferB.x) || Float.isNaN(bufferB.y)) {
				game.println("NaN encountered.");
				game.println(truePositionLast.x + "; " + truePositionLast.y);
				throw new RuntimeException("NaN");
			}
*/
			// position =
			// position.times(2.0).minus(positionLast).plus(accel.times(delta*delta));//Verlet
			// step - doesn't factor in lastdelta
			
			float deltasqr = delta*delta;
			position.x = position.x * 2.0f - positionLast.x + bufferAx*deltasqr;
			position.y = position.y * 2.0f - positionLast.y + bufferAy*deltasqr;
			//positionLast.set(g.bufferB);
			//truePositionLast.set(g.bufferB);
			positionLast.x = bufferBx;
			truePositionLast.x = bufferBx;
			positionLast.y = bufferBy;
			truePositionLast.y = bufferBy;
			
			forceZero();
			setLastDelta(delta);
			if (stuck){
				if (position.distanceTo(stuckPosition)>unstickDist){
					stuck = false;
				} else{
					position.set(stuckPosition);
				}
			}
		}
	}
	
	/*
	 * Check for and resolve collision.
	 * restCoef of 0 = inelastic, 1 = perfect bounce
	 * fricCoef of 0 = slippery, 1 = perfect grip
	 */
	
	public void collideTest(Particle p,float restCoef,float fricCoef){
		//if (true) return;
		if (p == this) return;
		//this.game.debugCounter++;
		float toc = Vector3d.circleCircleTOC(truePositionLast,position,p.truePositionLast,p.position,radius,p.radius);
		
		if (toc < -0.5f) return;

		//get collision locations
		float thisx = toc*this.position.x + (1.0f-toc)*this.truePositionLast.x;
		float thisy = toc*this.position.y + (1.0f-toc)*this.truePositionLast.y;
		float thatx = toc*p.position.x + (1.0f-toc)*p.truePositionLast.x;
		float thaty = toc*p.position.y + (1.0f-toc)*p.truePositionLast.y;
		
		//if toc == 0.0f, we might be starting in a penetrating state,
		//so we must project outwards first.
		
		//[fill in projection code]
		if (toc < .001f) { keepSeparate(p);
		//	if (p.mass > 50 || this.mass > 50) System.out.println("zero, hi-mass");
		return; }
		
		//normals
		float nx = (thatx-thisx)/(radius+p.radius);
		float ny = (thaty-thisy)/(radius+p.radius);
		//float nnorm = (float)Math.sqrt(nx*nx+ny*ny);
		//nx /= nnorm; ny /= nnorm;
		
		//tangents
		float tx = ny;
		float ty = -nx;
		
		float thisvx = this.position.x-this.truePositionLast.x;
		float thisvy = this.position.y-this.truePositionLast.y;
		float thatvx = p.position.x - p.truePositionLast.x;
		float thatvy = p.position.y - p.truePositionLast.y;
		//float pinx = (this.position.x-this.positionLast.x)*this.mass + (p.position.x-p.positionLast.x)*p.mass;
		//projections along normal/perp vectors
		float thisvn = thisvx*nx+thisvy*ny;
		float thisvt = thisvx*tx+thisvy*ty;
		float thatvn = thatvx*nx+thatvy*ny;
		float thatvt = thatvx*tx+thatvy*ty;
		
		if (thisvn-thatvn < 0f) return;
		
		//now resolve collisions in tangent and normal directions
		float thisvnout = ( (restCoef+1f)*p.mass*thatvn+thisvn*(this.mass-restCoef*p.mass) )/ (this.mass+p.mass);
		float thatvnout = ( (restCoef+1f)*this.mass*thisvn+thatvn*(p.mass-restCoef*this.mass) )/ (this.mass+p.mass);
		//for tangent "collision," restitution should be between -1 (slide) and 0 (stick)
		float c = fricCoef - 1.0f;
		float thisvtout = ( (c+1f)*p.mass*thatvt+thisvt*(this.mass-c*p.mass) )/ (this.mass+p.mass);
		float thatvtout = ( (c+1f)*this.mass*thisvt+thatvt*(p.mass-c*this.mass) )/ (this.mass+p.mass);
		//System.out.println(thisvn + " "+thisvnout);
		thisvx = thisvnout*nx+thisvtout*tx;
		thisvy = thisvnout*ny+thisvtout*ty;
		thatvx = thatvnout*nx+thatvtout*tx;
		thatvy = thatvnout*ny+thatvtout*ty;
		
		float rem = 1.0f-toc;
		this.position.x = thisx + rem*thisvx;
		this.position.y = thisy + rem*thisvy;
		p.position.x = thatx + rem*thatvx;
		p.position.y = thaty + rem*thatvy;
		
		this.positionLast.x = this.position.x - thisvx;
		this.positionLast.y = this.position.y - thisvy;
		p.positionLast.x = p.position.x - thatvx;
		p.positionLast.y = p.position.y - thatvy;
		//float poutx = this.mass*thisvx + p.mass*thatvx; 
			//(this.position.x-this.positionLast.x)*this.mass + (p.position.x-p.positionLast.x)*p.mass;
		//System.out.println(poutx-pinx+" "+this.myColor);
		
	}

	public void setLineDraw() {
		lineDraw = true;
	}

	public float distanceTo(Particle p){
		return position.distanceTo(p.position);
	}
	
	public void keepSeparate(Particle pB) {
		//breakable = true;
		//breakDistSqr = distanceSqr*1.3f;
		//returns true if constraint is already satisfied
		Particle pA = this;
		float tolerance = .001f;
		float distance = pA.radius+pB.radius;
		float distanceSqr = distance*distance;
		float myDistSqr = pA.position.distanceSqr(pB.position);
		if (myDistSqr > distanceSqr) return; //already separated
		float alpha = PApplet.sqrt(distanceSqr / (myDistSqr + .001f));
		float oneminusalpha = 1.0f - alpha;
		if (oneminusalpha < tolerance && oneminusalpha > -tolerance) {
			return;
		} else { // impose constraint
			float oneoversum = 1.0f / (pA.mass + pB.mass);
			float cmx = (pA.mass * pA.position.x + pB.mass * pB.position.x)
			* oneoversum;
			float cmy = (pA.mass * pA.position.y + pB.mass * pB.position.y)
			* oneoversum;
			float cmz = (pA.mass * pA.position.z + pB.mass * pB.position.z)
			* oneoversum;
			pA.position.x = cmx * oneminusalpha + alpha * pA.position.x;
			pA.position.y = cmy * oneminusalpha + alpha * pA.position.y;
			pA.position.z = cmz * oneminusalpha + alpha * pA.position.z;
			pB.position.x = cmx * oneminusalpha + alpha * pB.position.x;
			pB.position.y = cmy * oneminusalpha + alpha * pB.position.y;
			pB.position.z = cmz * oneminusalpha + alpha * pB.position.z;
		}
		return;
	}

}